package com.monochromebytes.mononet.virtual;

import java.nio.ByteBuffer;

public class Acknowledgment {

    public static final int      BIT_FIELD_LENGTH = Short.SIZE / 8;
    public static final int      BASE_LENGTH      = SequenceNumber.LENGTH;
    public static final int      LENGTH           = Acknowledgment.BASE_LENGTH
                                                          + Acknowledgment.BIT_FIELD_LENGTH;

    private final ByteBuffer     buffer;
    private final ByteBuffer     bitFieldBuffer;
    private final SequenceNumber base;

    public Acknowledgment() {
        this(ByteBuffer.allocate(Acknowledgment.LENGTH));
    }

    public Acknowledgment(ByteBuffer buffer) {
        assert (buffer.capacity() >= Acknowledgment.LENGTH); // TODO
        this.buffer = buffer.slice();

        // Map base sequence number.
        this.buffer.position(0);
        this.buffer.limit(Acknowledgment.BASE_LENGTH);
        this.base = new SequenceNumber(this.buffer);

        // Map bit field.
        this.buffer.position(Acknowledgment.BASE_LENGTH);
        this.buffer.limit(Acknowledgment.LENGTH);
        this.bitFieldBuffer = this.buffer.slice();
        
        this.buffer.clear();
    }

    public void addAcknowledge(SequenceNumber number) {
        int pos = number.getDistanceTo(this.base);
        if (pos > 0 && pos <= BASE_LENGTH * 8) {
            int value = this.bitFieldBuffer.getShort(0);
            value |= 1 << (pos - 1);
            this.bitFieldBuffer.putShort(0, (short) value);
        }
    }

    public boolean isAcknowledged(SequenceNumber number) {
        int pos = number.getDistanceTo(this.base);
        if (pos == 0) {
            return true;
        }
        
        if (pos > 0 && pos <= BASE_LENGTH * 8) {
            int value = this.bitFieldBuffer.getShort(0);
            int mask = 1 << (pos - 1);
            if ((value & mask) != 0) {
                return true;
            }
        }
        return false;
    }

    public void set(Acknowledgment other) {
        this.buffer.duplicate().put(other.buffer.duplicate());
    }

    public void setBase(SequenceNumber base) {
        // Shift the bitfield if necessary.
        int diff = this.base.getDistanceTo(base);
        if (diff > 0) {
            int value = ((this.bitFieldBuffer.getShort(0) & 0xFFFF) << 1) + 1;
            value = value << (diff - 1);
            this.bitFieldBuffer.putShort(0, (short) value);
        }
        else if (diff < 0) {
            int value = (this.bitFieldBuffer.getShort(0) & 0xFFFF) >> diff;
            this.bitFieldBuffer.putShort(0, (short) value);
        }
        // Set base sequence number.
        this.base.set(base);
    }

    public ByteBuffer getBuffer() {
        return this.buffer.duplicate();
    }
    

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.base == null) ? 0 : this.base.hashCode());
        result = prime * result + ((this.bitFieldBuffer == null) ? 0 : this.bitFieldBuffer.hashCode());
        result = prime * result + ((this.buffer == null) ? 0 : this.buffer.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Acknowledgment other = (Acknowledgment) obj;
        if (this.base == null) {
            if (other.base != null)
                return false;
        }
        else if (!this.base.equals(other.base))
            return false;
        if (this.bitFieldBuffer == null) {
            if (other.bitFieldBuffer != null)
                return false;
        }
        else if (!this.bitFieldBuffer.equals(other.bitFieldBuffer))
            return false;
        if (this.buffer == null) {
            if (other.buffer != null)
                return false;
        }
        else if (!buffer.equals(other.buffer))
            return false;
        return true;
    }

    // TODO: remove
    @Override
    public String toString() {
        String stringBase = Integer.toBinaryString(this.buffer.getShort(0));
        while (stringBase.length() < Integer.SIZE) {
            stringBase = "0" + stringBase;
        }
        
        String stringBitField = Integer.toBinaryString(this.buffer.getShort(2));
        while (stringBitField.length() < Integer.SIZE) {
            stringBitField = "0" + stringBitField;
        }
        
        return "Base: " + stringBase + " Bitfield: " + stringBitField;
    }
}
